-- |
-- Description: Add/Drop computed fields in metadata
module Hasura.RQL.DDL.ComputedField
  ( AddComputedField (..),
    ComputedFieldDefinition (..),
    runAddComputedField,
    DropComputedField,
    runDropComputedField,
    dropComputedFieldInMetadata,
  )
where

import Data.Aeson
import Data.HashMap.Strict.InsOrd qualified as OMap
import Data.Text.Extended
import Hasura.Base.Error
import Hasura.EncJSON
import Hasura.Prelude
import Hasura.RQL.DDL.Permission
import Hasura.RQL.Types
import Hasura.SQL.AnyBackend qualified as AB

data AddComputedField b = AddComputedField
  { _afcSource :: !SourceName,
    _afcTable :: !(TableName b),
    _afcName :: !ComputedFieldName,
    _afcDefinition :: !(ComputedFieldDefinition b),
    _afcComment :: !(Maybe Text)
  }
  deriving stock (Generic)

instance (Backend b) => ToJSON (AddComputedField b) where
  toJSON = genericToJSON hasuraJSON

instance (Backend b) => FromJSON (AddComputedField b) where
  parseJSON = withObject "AddComputedField" $ \o ->
    AddComputedField
      <$> o .:? "source" .!= defaultSource
      <*> o .: "table"
      <*> o .: "name"
      <*> o .: "definition"
      <*> o .:? "comment"

runAddComputedField ::
  forall b m.
  (BackendMetadata b, MonadError QErr m, CacheRWM m, MetadataM m) =>
  AddComputedField b ->
  m EncJSON
runAddComputedField q = do
  void $ withPathK "table" $ askTabInfo @b source table
  let metadataObj =
        MOSourceObjId source $
          AB.mkAnyBackend $
            SMOTableObj @b table $
              MTOComputedField computedFieldName
      metadata = ComputedFieldMetadata computedFieldName (_afcDefinition q) (_afcComment q)
  buildSchemaCacheFor metadataObj $
    MetadataModifier $
      tableMetadataSetter source table . tmComputedFields
        %~ OMap.insert computedFieldName metadata
  pure successMsg
  where
    source = _afcSource q
    table = _afcTable q
    computedFieldName = _afcName q

data DropComputedField b = DropComputedField
  { _dccSource :: !SourceName,
    _dccTable :: !(TableName b),
    _dccName :: !ComputedFieldName,
    _dccCascade :: !Bool
  }

instance (Backend b) => FromJSON (DropComputedField b) where
  parseJSON = withObject "DropComputedField" $ \o ->
    DropComputedField
      <$> o .:? "source" .!= defaultSource
      <*> o .: "table"
      <*> o .: "name"
      <*> o .:? "cascade" .!= False

runDropComputedField ::
  forall b m.
  (QErrM m, CacheRWM m, MetadataM m, BackendMetadata b) =>
  DropComputedField b ->
  m EncJSON
runDropComputedField (DropComputedField source table computedField cascade) = do
  -- Validation
  fields <- withPathK "table" $ _tciFieldInfoMap <$> askTableCoreInfo @b source table
  void $ withPathK "name" $ askComputedFieldInfo fields computedField

  -- Dependencies check
  sc <- askSchemaCache
  let deps =
        getDependentObjs sc $
          SOSourceObj source $
            AB.mkAnyBackend $
              SOITableObj @b table $
                TOComputedField computedField
  when (not cascade && not (null deps)) $ reportDependentObjectsExist deps

  withNewInconsistentObjsCheck do
    metadataModifiers <- mapM purgeComputedFieldDependency deps
    buildSchemaCache $
      MetadataModifier $
        tableMetadataSetter @b source table
          %~ dropComputedFieldInMetadata computedField . foldl' (.) id metadataModifiers
  pure successMsg
  where
    purgeComputedFieldDependency = \case
      -- TODO: do a better check of ensuring that the dependency is as expected.
      -- i.e, the only allowed dependent objects on a computed fields are permissions
      -- on the same table
      SOSourceObj _ exists
        | Just (SOITableObj _ (TOPerm roleName permType)) <-
            AB.unpackAnyBackend @b exists ->
          pure $ dropPermissionInMetadata roleName permType
      d ->
        throw500 $
          "unexpected dependency for computed field "
            <> computedField <<> "; "
            <> reportSchemaObj d
